<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\GooglePlay;

use Exception;
use Srv\Conf\ConfModel\GPlayAuthConf;
use Srv\Libs\Common\CommFile;
use Srv\Libs\Common\CommJson;
use Srv\Libs\Common\CommNet;
use Srv\Libs\Common\CommString;
use Srv\Libs\Common\CommTime;
use Srv\Libs\ConfModel\NetworkConf;
use Srv\Libs\Frame\Conf;
use Srv\Libs\Frame\ControllerAbstract;
use Srv\Libs\Plugins\Jwt\JWT;
use Srv\Libs\Storage\DataRedis;

final class GoogleSub
{
    private string $androidBundleId                 = '';
    private string $projectId                       = '';
    private string $secretKeyFile                   = '';
    private ?DataRedis $CcAuto                      = null;             // CcAuto
    private const GPLAY_AUTH_PREFIX                 = 'GPLAY_AUTH_SUB_';// 缓存令牌前缀

    /**
     * @param GPlayAuthConf $GPlayAuthConf
     * __construct
     */
    public function __construct(GPlayAuthConf $GPlayAuthConf)
    {
        $this->androidBundleId      = $GPlayAuthConf->getAndroidBundleId();
        $this->projectId            = $GPlayAuthConf->getProjectId();
        $this->secretKeyFile        = $GPlayAuthConf->getSecretKeyFile();
        $this->CcAuto               = ControllerAbstract::getCacheAuto();
    }

    /**
     * @param string $projectId
     * @param string $subName
     * @param int $maxNum
     * @return array
     * getPullList
     */
    public function getPullList(string $projectId, string $subName, int $maxNum):array
    {
        if(strlen($projectId) < 3 || strlen($subName) < 3 || $maxNum < 1) return [];
        $accessToken    = $this->getAccessToken();
        if(strlen($accessToken) < 1) return [];
        $url            = 'https://pubsub.googleapis.com/v1/projects/{PROJECT_ID}/subscriptions/{SUB_NAME}:pull';
        $url            = CommString::replaceTplStr($url, ['PROJECT_ID' => $projectId, 'SUB_NAME' => $subName]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'POST', 'POST_DATA' => CommJson::encodeArray(['maxMessages' => $maxNum]), 'AUTH_TYPE' => 'Bearer', 'PASS' => $accessToken, 'HEADER' => ['Content-Type' => 'application/json']];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('appGG');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo    = CommNet::getUrlInfo($url, $param);    // https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.subscriptions/pull
            $bodyStr    = trim($urlInfo['body']??'');
            $bodyData   = CommJson::decodeArray($bodyStr);      // https://developer.android.com/google/play/billing/rtdn-reference?hl=zh-cn
            $httpCode   = intval($urlInfo['http_code']??-1);
            if($httpCode === 200) break;
            if(isset($bodyData['receivedMessages']) || isset($bodyData['error']) || count($bodyData) > 0) break;
        }while(--$tryNum >= 0);
        if(isset($bodyData['error'])) return [];
        $receivedMessages   = $bodyData['receivedMessages']??[];
        if(!is_array($receivedMessages) || !isset($receivedMessages[0])) return [];
        return $receivedMessages;
    }

    /**
     * @param string $projectId
     * @param string $subName
     * @param array $ackIdList
     * @return bool
     * acknowledge
     */
    public function acknowledge(string $projectId, string $subName, array $ackIdList):bool
    {
        if(strlen($projectId) < 3 || strlen($subName) < 3 || count($ackIdList) < 1) return false;
        $accessToken    = $this->getAccessToken();
        if(strlen($accessToken) < 1) return false;
        $url            = 'https://pubsub.googleapis.com/v1/projects/{PROJECT_ID}/subscriptions/{SUB_NAME}:acknowledge';
        $url            = CommString::replaceTplStr($url, ['PROJECT_ID' => $projectId, 'SUB_NAME' => $subName]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'POST', 'POST_DATA' => CommJson::encodeArray(['ackIds' => $ackIdList]), 'AUTH_TYPE' => 'Bearer', 'PASS' => $accessToken, 'HEADER' => ['Content-Type' => 'application/json']];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('appGG');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo    = CommNet::getUrlInfo($url, $param);    // https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.subscriptions/acknowledge
            $bodyStr    = trim($urlInfo['body']??'');
            $httpCode   = intval($urlInfo['http_code']??-1);
            if(in_array($httpCode, [204, 200], true) && strlen($bodyStr) < 3) return true;
            if(in_array($httpCode, [204, 200], true) || strlen($bodyStr) > 0) break;
        }while(--$tryNum >= 0);
        return false;
    }

    /**
     * @return string
     * getAccessToken
     */
    private function getAccessToken():string
    {
        $content            = '';
        if(!CommFile::getContent($this->secretKeyFile, $content)) return '';
        $jsonKey            = CommJson::decodeArray($content);
        $iss                = strval($jsonKey['client_email']??'');
        if(strlen($iss) < 1) return '';
        $accessTokenData    = [];
        $cacheKey           = self::GPLAY_AUTH_PREFIX.md5($iss.($jsonKey['private_key_id']??''));
        $cacheJsonStr       = $this->CcAuto->get($cacheKey);
        if(is_string($cacheJsonStr)) $accessTokenData = CommJson::decodeArray($cacheJsonStr);
        if(count($accessTokenData) < 1 || !isset($accessTokenData['access_token'])){
            $currTime   = CommTime::getTimeStamp();
            $payload    = [
                'iss'       => $iss,
                'exp'       => ($currTime + 3600),
                'iat'       => $currTime - 60,
                'aud'       => 'https://oauth2.googleapis.com/token',
                'scope'     => 'https://www.googleapis.com/auth/pubsub',
            ];
            $privateKey = strval($jsonKey['private_key']??'');
            try{
                $assertion  = JWT::encode($payload, $privateKey, 'RS256', null, []);
            }catch (Exception $Exception){
                return '';
            }
            $postData       = ['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $assertion];
            $param          = ['TIMEOUT' => 10, 'METHOD' => 'POST', 'POST_DATA' => $postData];
            $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('appGG');
            if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
            $tryNum         = 2;
            do{
                $urlInfo    = CommNet::getUrlInfo('https://oauth2.googleapis.com/token', $param);
                $bodyData   = CommJson::decodeArray(trim($urlInfo['body']??''));    // {"access_token":"","expires_in":3599,"token_type":"Bearer"}
                if(isset($bodyData['access_token']) || isset($bodyData['error']) || count($bodyData) > 0) break;
            }while(--$tryNum >= 0);
            $accessTokenData    = $bodyData;
            $accessTokenData['_ISS'] = $iss;
            $accessToken        = strval($accessTokenData['access_token']??'');
            $expiresIn          = intval($accessTokenData['expires_in']??-1);
            if(strlen($accessToken) > 0 && count($accessTokenData) > 0) $this->CcAuto->set($cacheKey, CommJson::encodeArray($accessTokenData), min(max($expiresIn - 600, 60), 3600));
        }else{
            $accessToken        = strval($accessTokenData['access_token']??'');
        }
        return $accessToken;
    }
}